import { Nodes, dom } from '@knno/dom';
import { Progress } from './progress/progress';
import { DATA } from './symbol';
import '../dict';
import { DICT } from '../dict';

function httpError(code: number): string {
	if (code === 0) return DICT.networkError;
	if (code >= 400 && code < 500) return DICT.badRequest;
	return DICT.serverError;
}

function isResponseJson(xhr: XMLHttpRequest): boolean {
	const contentType = xhr.getResponseHeader('Content-Type') ?? '';
	return /^application\/json(\s*;.*)?$/.test(contentType.trim());
}

function getResponseHeaders(xhr: XMLHttpRequest): Record<string, string> {
	const headers = xhr.getAllResponseHeaders();
	const arr = headers.trim().split(/[\r\n]+/);
	const headerMap: Record<string, string> = {};
	arr.forEach((line) => {
		const parts = line.split(': ');
		const header = parts.shift();
		if (header) {
			const value = parts.join(': ');
			headerMap[header] = value;
		}
	});
	return headerMap;
}

function getResponseError(xhr: XMLHttpRequest): string {
	const contentType = xhr.getResponseHeader('Content-Type') ?? '';
	if (/^text\/plain(\s*;.*)?$/.test(contentType.trim())) {
		return xhr.responseText;
	} else {
		return `${xhr.status}: ${httpError(xhr.status)}`;
	}
}

export interface UploadOption<T> {
	showProgress?: boolean;
	onProgress?: (xhr: XMLHttpRequest, loaded: number, total: number) => void;
	onStart?: (xhr: XMLHttpRequest) => void;
	onComplete?: (xhr: XMLHttpRequest, response: ServerResponse<T>) => void;
	onError?: (xhr: XMLHttpRequest, error: string) => void;
	onAbort?: () => void;
}

export interface ServerResponse<T> {
	code: number;
	headers: Record<string, string>;
	data: T | string;
}

export class Uploader extends Nodes {
	[DATA]: {
		resolver?: (files: File[]) => void;
	} = {};
	constructor() {
		super(dom.input().attr('type', 'file'));
		this.on('change', () => {
			if (typeof this[DATA].resolver === 'function') {
				const fn = this[DATA].resolver;
				fn(this.files());
				delete this[DATA].resolver;
			}
		});
	}

	multple(): boolean;
	multple(value: boolean): this;
	multple(value?: boolean): this | boolean {
		if (value === undefined) {
			return this.prop('multiple') ?? false;
		}
		this.prop('multiple', !!value);
		return this;
	}

	accept(): string;
	accept(value: string): this;
	accept(value?: string): this | string {
		if (value === undefined) {
			return this.prop('accept') ?? '';
		}
		this.prop('accept', value + '');
		return this;
	}

	/**
	 * Only worked in event handler call
	 */
	select(): Promise<File[]> {
		return new Promise<File[]>((resolve, reject) => {
			this[DATA].resolver = resolve;
			try {
				(this.elems()[0] as HTMLInputElement).click();
			} catch (e) {
				reject(`${e}`);
			}
		});
	}

	files(): File[] {
		return Array.from((this.elems()[0] as HTMLInputElement).files ?? []);
	}

	formData(extra?: { [index: string]: string }): FormData {
		const fd = new FormData();
		this.files().forEach((f) => {
			fd.append(this.name() ?? 'file', f);
		});
		if (extra) {
			for (const key in Object.getOwnPropertyNames(extra)) {
				fd.append(key, extra[key]);
			}
		}
		return fd;
	}

	upload<T>(address: string, options?: UploadOption<T>): Promise<ServerResponse<T>> {
		if (this.files().length === 0) {
			if (typeof options?.onAbort === 'function') {
				options.onAbort();
			}
			return Promise.reject(DICT.noFiles);
		}
		return upload(address, this.formData(), options);
	}
}

export function readAsDataURL(file: File): Promise<string> {
	return new Promise<string>((resolve, reject) => {
		const reader = new FileReader();
		reader.addEventListener('load', () => {
			if (typeof reader.result === 'string') {
				resolve(reader.result);
			} else {
				reject('cannot read as data url');
			}
		});
		reader.addEventListener('error', () => {
			reject(reader.error ? reader.error + '' : 'cannot read as data url');
		});
		reader.readAsDataURL(file);
	});
}

export async function download(file: File): Promise<void> {
	const url = await readAsDataURL(file);
	dom.a().attr('href', url).attr('download', file.name);
}

export function upload<T = unknown>(
	address: string,
	formData: FormData,
	options?: UploadOption<T>
): Promise<ServerResponse<T>> {
	let closed = false;
	if (options?.showProgress) {
		Progress.show();
		Progress.update(0, DICT.uploading);
	}
	function closeProgress() {
		if (!closed && options?.showProgress) {
			closed = true;
			Progress.close();
		}
	}
	return new Promise<ServerResponse<T>>((resove, reject) => {
		const xhr = new XMLHttpRequest();
		function failError(errMsg: string) {
			if (typeof options?.onError === 'function') {
				options.onError(xhr, errMsg);
			}
			reject(errMsg);
		}
		xhr.upload.addEventListener(
			'progress',
			(e) => {
				if (e.lengthComputable) {
					if (typeof options?.onProgress === 'function') {
						options.onProgress(xhr, e.loaded, e.total);
					}
					if (options?.showProgress) {
						const percentComplete = Math.round((e.loaded * 100) / e.total);
						Progress.update(percentComplete);
					}
				}
			},
			false
		);
		xhr.addEventListener(
			'load',
			() => {
				closeProgress();
				if (xhr.status != 200 && xhr.status != 204) {
					const errMsg = getResponseError(xhr);
					failError(errMsg);
				} else {
					try {
						const headers = getResponseHeaders(xhr);
						let serverResponse: ServerResponse<T>;
						if (isResponseJson(xhr)) {
							const result = JSON.parse(xhr.responseText);
							serverResponse = {
								code: xhr.status,
								headers: headers,
								data: result,
							};
						} else {
							serverResponse = {
								code: xhr.status,
								headers: headers,
								data: xhr.responseText,
							};
						}
						if (typeof options?.onComplete === 'function') {
							options.onComplete(xhr, serverResponse);
						}
						resove(serverResponse);
					} catch (e) {
						failError(DICT.error + e);
					}
				}
			},
			false
		);
		xhr.addEventListener(
			'error',
			(e) => {
				closeProgress();
				failError(DICT.error + e);
			},
			false
		);
		xhr.addEventListener(
			'abort',
			() => {
				closeProgress();
				if (typeof options?.onAbort === 'function') {
					options.onAbort();
				}
				reject(DICT.aborted);
			},
			false
		);
		try {
			xhr.open('POST', address);
			xhr.send(formData);
			if (typeof options?.onStart === 'function') {
				options.onStart(xhr);
			}
		} catch (e) {
			closeProgress();
			failError(DICT.error + e);
		}
	});
}
